﻿/*
* Copyright 2009-2010 AKABANA.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
using System;
using System.Globalization;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Akabana.Amf.Amf0.IO;
using Akabana.Amf.Amf3.IO;
using Akabana.Amf.Exception;
using Akabana.Amf.IO;
using Akabana.Amf.Serializer;
using Akabana.Amf.Util;

namespace Akabana.Amf.Amf3.Serializer
{
    public class Amf3Serializer : AbstractAmfSerializer
    {

        public Amf3Serializer(AmfWriter amfWriter)
            : base(amfWriter)
        {
            
        }
        private const int NO_REFERENCE = -1;

        private const string VALUE_TYPE_FIELD = "value";

        public override void Serialize(object value)
        {
            this.referenceClear();
            this.DoEncode(value);
        }

        public void DoEncode(object value)
        {
            if (value == null)
            {
                this.DoEncodeNull(amfWriter);
                return;
            }
            var objectType = value.GetType();
            do
            {
                if (objectType.IsAMFPrimitive())
                {
                    DoEncodeRefType(value, ref amfWriter);
                    break;
                }
                if (objectType.IsEnum)
                {
                    DoEncodeValueType(value, ref amfWriter);
                    break;
                }
                if (objectType.IsValueType)
                {
                    DoEncodeValueType(value, ref amfWriter);
                    break;
                }

                DoEncodeRefType(value, ref amfWriter);
            } while (false);
        }

        private void DoEncodeValueType(object value, ref AmfWriter amfWriter)
        {
            this.DoEncodeClassObject(value);
        }

        private void DoEncodeRefType(object value, ref AmfWriter amfWriter)
        {
            Type objectType = value.GetType();
            TypeCode objectCode = Type.GetTypeCode(objectType);
            switch (objectCode)
            {
                case TypeCode.Boolean:
                    this.DoEncodeBoolean((bool)value);
                    break;
                case TypeCode.Double:
                case TypeCode.Decimal:
                case TypeCode.UInt64:
                case TypeCode.Int64:
                    this.DoEncodeNumber(Convert.ToDouble(value));
                    break;
                case TypeCode.UInt32:
                case TypeCode.Int32:
                    this.DoEncodeInteger(Convert.ToInt32(value));
                    break;

                case TypeCode.Int16:
                case TypeCode.UInt16:
                    this.DoEncodeInteger(Convert.ToInt32(value));
                    break;

                case TypeCode.Byte:
                case TypeCode.SByte:
                    this.DoEncodeInteger(Convert.ToInt32(value));
                    break;
                case TypeCode.String:
                    this.DoEncodeStirng((string)value);
                    break;
                case TypeCode.DateTime:
                    this.DoEncodeDate((DateTime)value);
                    break;
                case TypeCode.Object:
                    if (objectType.IsArray)
                    {
                        if (Type.GetTypeCode(objectType.GetElementType()) == TypeCode.Byte)
                        {
                            this.DoEncodeByteArray((byte[])value);
                        }
                        else
                        {
                            this.DoEncodeArray((Array)value);
                        }
                    }
                    else
                    {
                        if (value is IDictionary)
                        {
                            this.DoEncodeAnonymousObject(value as IDictionary);
                        }
                        else
                        {
                            this.DoEncodeClassObject(value);
                        }
                    }
                    break;
                default:
                    throw new UnsupportTypeException(objectType.Name);
            }
        }

        private void DoEncodeInteger(int value)
        {
            if (value >= -0x10000000 && value < 0x10000000)
            {
                amfWriter.WriteTypeMarker(Amf3TypeMarker.IntegerMarker);
                amfWriter.WriteS29(value);
            }
            else
            {
                this.DoEncodeNumber(value);
            }
        }

        private void DoEncodeNumber(double number)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.DoubleMarker);
            amfWriter.WriteNumber(number);
        }

        private void DoEncodeStirng(string value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.StringMarker);
            this.DoEncodeUtfStirng(value);
        }

        private void DoEncodeUtfStirng(string value)
        {
            var byteData = Encoding.UTF8.GetBytes(value);
            var stringSize = byteData.Length;

            if (stringSize >= 0xfffffff)
            {
                throw new StringOutOfRangeException(stringSize);
            }

            if (value.Length == 0)
            {
                amfWriter.Write(Amf3Consts.EMPTY_STRING);
            }
            else
            {
                var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
                var referenceIndex = referenceTable.GetStringIndex(value);

                if (referenceIndex == NO_REFERENCE)
                {
                    referenceTable.SetString(value);

                    stringSize = (stringSize << Amf3Consts.BIT_SHIFT_REFERENCE) | Amf3Consts.BIT_VALUE;
                    amfWriter.WriteU29(stringSize);
                    amfWriter.Write(byteData);
                }
                else
                {
                    amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
                }
            }
        }

        private void DoEncodeBoolean(bool value)
        {
            if (value)
            {
                amfWriter.WriteTypeMarker(Amf3TypeMarker.TrueMarker);
            }
            else
            {
                amfWriter.WriteTypeMarker(Amf3TypeMarker.FalseMarker);
            }
        }

        private void DoEncodeDate(DateTime value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.DateMarker);

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            var referenceIndex = referenceTable.GetObjectIndex(value);

            if (referenceIndex == NO_REFERENCE)
            {
                referenceTable.SetObject(value);
                amfWriter.WriteU29(Amf3Consts.BIT_VALUE);
                double time = DateUtil.ToUtcMillSec(value);
                amfWriter.WriteNumber(time);
            }
            else
            {
                amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
            }
        }

        private void DoEncodeNull(AmfWriter amfWriter)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.NullMarker);
        }

        private void DoEncodeByteArray(byte[] value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.ByteArrayMarker);

            var size = value.Length;

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            var referenceIndex = referenceTable.GetObjectIndex(value);

            if (referenceIndex == NO_REFERENCE)
            {
                referenceTable.SetObject(value);
                size = (size << Amf3Consts.BIT_SHIFT_REFERENCE) | Amf3Consts.BIT_VALUE;
                amfWriter.WriteU29(size);
                amfWriter.Write(value);
            }
            else
            {
                amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
            }
        }

        private void DoEncodeAnonymousObject(IDictionary value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.ObjectMarker);

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            var referenceIndex = referenceTable.GetObjectIndex(value);

            if (referenceIndex == NO_REFERENCE)
            {
                referenceTable.SetObject(value);

                var dataDefinition = Amf3Consts.BIT_VALUE | Amf3Consts.BIT_NON_EXT | Amf3Consts.BIT_NON_DYNAMIC | Amf3Consts.BIT_TRAITS_VALUE;
                dataDefinition |= (value.Count << Amf3Consts.BIT_SHIFT_NUM_FIELDS);
                amfWriter.WriteU29(dataDefinition);
                DoEncodeUtfStirng("");

                ICollection keys = value.Keys;
                foreach (String key in keys)
                {
                    this.DoEncodeUtfStirng(key);
                }

                foreach (String key in keys)
                {
                    this.DoEncode(value[key]);
                }
            }
            else
            {
                amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
            }
        }

        private void DoEncodeClassObject(object value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.ObjectMarker);

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            var referenceIndex = referenceTable.GetObjectIndex(value);

            if (referenceIndex == NO_REFERENCE)
            {
                referenceTable.SetObject(value);
                do
                {
                    var className = TypeResolver.FindResponseTypeName(value.GetType().FullName);
                    if (value is IExternalizable)
                    {
                        var dataDefinition = Amf3Consts.BIT_VALUE | Amf3Consts.BIT_MASK_TRAITS | Amf3Consts.BIT_EXT;
                        amfWriter.WriteU29(dataDefinition);
                        DoEncodeUtfStirng(className);
                        (value as IExternalizable).writeExternal(amfWriter);
                        break;
                    }
                    {
                        var referenceTraitsIndex = referenceTable.GetTraitsIndex(className);
                        Traits traits = null;
                        if (referenceTraitsIndex == NO_REFERENCE)
                        {
                            traits = new Traits();
                            traits.className = className;
                            referenceTable.SetTraits(traits);
                            this.DoEncodeTraits(traits, value.GetType());
                        }
                        else
                        {
                            traits = referenceTable.GetTraits(referenceTraitsIndex);
                            var dataDefinition = Amf3Consts.BIT_VALUE | Amf3Consts.BIT_NON_EXT | Amf3Consts.BIT_NON_DYNAMIC | Amf3Consts.BIT_TRAITS_REFERENCE;
                            dataDefinition |= (referenceTraitsIndex << Amf3Consts.BIT_SHIFT_TRAITS);
                            amfWriter.WriteU29(dataDefinition);
                        }
                        this.DoEncodeTraitsObject(traits, value);
                    }

                } while (false);

            }
            else
            {
                amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
            }
        }

        private void DoEncodeTraits(Traits traits, Type type)
        {
            var properties = type.GetProperties();
            var fields = type.GetFields();
            var numProperty = properties.Length;
            var numField = fields.Length;
            var traitsFields = new List<string>();

            if (type.IsAMFValueType())
            {
                traitsFields.Add(VALUE_TYPE_FIELD);
            }
            else
            {
                for (int i = 0; i < numProperty; i++)
                {
                    var propertyInfo = properties[i];
                    if (propertyInfo.CanRead)
                    {
                        traitsFields.Add(propertyInfo.Name);
                    }
                }
                for (int i = 0; i < numField; i++)
                {
                    var fieldInfo = fields[i];
                    if (fieldInfo.IsPublic &&
                        !fieldInfo.IsStatic &&
                        !fieldInfo.IsLiteral &&
                        !fieldInfo.IsInitOnly &&
                        !fieldInfo.IsNotSerialized &&
                        !fieldInfo.IsSpecialName
                        )
                    {

                        traitsFields.Add(fieldInfo.Name);
                    }
                }
            }

            var dataDefinition = Amf3Consts.BIT_VALUE | Amf3Consts.BIT_NON_EXT | Amf3Consts.BIT_NON_DYNAMIC | Amf3Consts.BIT_TRAITS_VALUE;
            dataDefinition |= (traitsFields.Count << Amf3Consts.BIT_SHIFT_NUM_FIELDS);
            amfWriter.WriteU29(dataDefinition);

            if (traits.className == null)
            {
                traits.className = type.FullName;
            }

            this.DoEncodeUtfStirng(traits.className);

            foreach (var item in traitsFields)
            {
                this.DoEncodeUtfStirng(item);
            }

            traits.fields = traitsFields.ToArray();

        }

        private void DoEncodeTraitsObject(Traits traits, object value)
        {
            Type valueType = value.GetType();
            if (valueType.IsAMFValueType())
            {
                if (valueType.IsEnum)
                {
                    DoEncodeRefType(value, ref amfWriter);
                }
                else
                {
                    DoEncodeRefType(value.ToString(), ref amfWriter);
                }
            }
            else
            {
                foreach (var item in traits.fields)
                {
                    var field = valueType.GetField(item, BindingFlags.Public | BindingFlags.Instance);
                    if (field != null)
                    {
                        this.DoEncode(field.GetValue(value));
                        continue;
                    }
                    var property = valueType.GetProperty(item, BindingFlags.Public | BindingFlags.Instance);
                    if (property != null && property.CanRead)
                    {
                        this.DoEncode(property.GetValue(value, null));
                        continue;
                    }
                }
            }
        }

        private void DoEncodeArray(Array value)
        {
            amfWriter.WriteTypeMarker(Amf3TypeMarker.ArrayMarker);

            var size = value.Length;

            var referenceTable = (Amf3ReferenceTable)GetReferenceTable();
            var referenceIndex = referenceTable.GetObjectIndex(value);

            if (referenceIndex == NO_REFERENCE)
            {
                referenceTable.SetObject(value);
                size = (size << Amf3Consts.BIT_SHIFT_REFERENCE) | Amf3Consts.BIT_VALUE;
                amfWriter.WriteU29(size);
                amfWriter.Write(Amf3Consts.EMPTY_STRING);
                foreach (var item in value)
                {
                    this.DoEncode(item);
                }
            }
            else
            {
                amfWriter.WriteU29(referenceIndex << Amf3Consts.BIT_SHIFT_REFERENCE);
            }
        }
        protected override IAmfReferenceTable CreateReferenceTable()
        {
            return new Amf3ReferenceTable();
        }
    }
}
